/*
 * 07/10/2011 - 00:02
 */
package org.nettyflux.udp;

import org.nettyflux.netmessages.NetworkMessage;
import org.nettyflux.mailbox.MailBox;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import org.nettyflux.communicator.ListeningThread;
import org.nettyflux.communicator.Receiver;
import org.nettyflux.debug.GlobalDebugger;
import org.nettyflux.mailbox.ReceiveListener;
import org.nettyflux.netmessages.MessageReceived;
import org.nettyflux.netmessages.SendCommand;

/**
 * @author Victor Williams Stafusa da Silva
 */
public class UDPCommunicator implements Receiver<MessageReceived<UDPHost>> {

    private final DatagramSocket socket;
    private final int localPort;
    private final int receiveBufferSize;

    private final MailBox<NetworkMessage> mailbox;
    private final UDPClientSenderListener senderListener;
    private final UDPClientCloseListener closeListener;

    public UDPCommunicator(MailBox<NetworkMessage> mailbox, int localPort, boolean autoReceive) throws SocketException {
        this(mailbox, 64 * 1024, localPort, autoReceive);
    }

    public UDPCommunicator(MailBox<NetworkMessage> mailbox, int bufferSize, int localPort, boolean autoReceive) throws SocketException {
        this(mailbox, bufferSize, new DatagramSocket(localPort), autoReceive);
    }

    public UDPCommunicator(MailBox<NetworkMessage> mailbox, int bufferSize, DatagramSocket socket, boolean autoReceive) throws SocketException {
        this.socket = socket;
        this.localPort = socket.getLocalPort();
        this.receiveBufferSize = bufferSize;
        this.mailbox = mailbox;
        this.senderListener = new UDPClientSenderListener();
        this.closeListener = new UDPClientCloseListener();
        this.mailbox.addReceiveListener(SendCommand.class, senderListener);
        this.mailbox.addReceiveListener(UDPCloseConnectionCommand.class, closeListener);
        if (autoReceive) ListeningThread.create(this);
    }

    public DatagramSocket getSocket() {
        return socket;
    }

    public void send(UDPHost target, byte[] message) throws IOException {
        if (isClosed()) throw new IllegalStateException("The socket is closed.");
        if (target.getLocalPort() != localPort) {
            throw new IllegalArgumentException("Incorrect localport.");
        }

        if (GlobalDebugger.isEnabled()) {
            GlobalDebugger.println("Sending UDP: " + java.util.Arrays.toString(message));
        }

        DatagramPacket packet = new DatagramPacket(message, message.length,
                target.getAddress(), target.getRemotePort());

        socket.send(packet);
    }

    @Override
    public synchronized MessageReceived<UDPHost> receive() throws IOException {
        if (isClosed()) throw new IllegalStateException("The socket is closed.");

        if (GlobalDebugger.isEnabled()) {
            GlobalDebugger.println("Will receive UDP in port " + localPort + ".");
        }

        byte[] message = new byte[receiveBufferSize];
        DatagramPacket packet = new DatagramPacket(message, receiveBufferSize);
        socket.receive(packet);
        byte[] answer = new byte[packet.getLength()];
        System.arraycopy(message, 0, answer, 0, answer.length);

        if (GlobalDebugger.isEnabled()) {
            GlobalDebugger.println("Received UDP: " + java.util.Arrays.toString(message));
        }

        UDPHost sender = new UDPHost(packet.getAddress(), localPort, packet.getPort());
        MessageReceived<UDPHost> nmessage = MessageReceived.create(sender, answer);
        mailbox.postMessage(nmessage);
        return nmessage;
    }

    @Override
    public void close() throws IOException {
        mailbox.removeReceiveListener(senderListener);
        mailbox.removeReceiveListener(closeListener);
        socket.close();
    }

    @Override
    public boolean isClosed() {
        return socket.isClosed();
    }

    private class UDPClientCloseListener implements ReceiveListener<UDPCloseConnectionCommand> {

        @Override
        public void receive(UDPCloseConnectionCommand message) {
            if (message.getPort() == localPort) {
                try {
                    close();
                    mailbox.postMessage(message.makeAck());
                } catch (IOException e) {
                    mailbox.postMessage(message.makeFailAck(e));
                }
            }
        }
    }

    private class UDPClientSenderListener implements ReceiveListener<SendCommand<?>> {

        @Override
        public void receive(SendCommand<?> message) {
            if (!(message.getReceiver() instanceof UDPHost)) return;
            UDPHost host = (UDPHost) message.getReceiver();
            if (host.getLocalPort() == localPort) {
                try {
                    send(host, message.getMessage());
                    mailbox.postMessage(message.makeAck());
                } catch (IOException e) {
                    mailbox.postMessage(message.makeFailAck(e));
                }
            }
        }
    }
}
